
#include "Window.hpp"
#include "Platform.hpp"

namespace Data {
class ImpCallbacks {
public:
  static void windowSizeCallback(GLFWwindow *pglfwWindow, int width, int height) {
    if (width == 0 || height == 0)
      return;

    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      pwindow->setWindowSize(width, height);
    }
  }

  static void keyboardCallback(GLFWwindow *pglfwWindow, int key, int scancode, int action,
                               int modifiers) {
    // TODO:
    KeyboardEvent event;
    if (prepareKeyboardEvent(key, action, modifiers, event)) {
      Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
      if (pwindow) {
        pwindow->mp_callbacks->keyboardEvent(event);
      }
    }
  }

  static void charInputCallback(GLFWwindow *pglfwWindow, uint32_t input) {
    KeyboardEvent event;
    event.type = KeyboardEvent::Type::Input;
    event.codePoint = input;

    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      pwindow->mp_callbacks->keyboardEvent(event);
    }
  }

  static void mouseMoveCallback(GLFWwindow *pglfwWindow, double mouseX, double mouseY) {
    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      MouseEvent event;
      event.type = MouseEvent::Type::Move;
      event.pos = calcMousePos(mouseX, mouseY, pwindow->getMouseScale());
      event.screenPos = {mouseX, mouseY};
      event.wheelData = glm::vec2(0.0f);

      pwindow->mp_callbacks->mouseEvent(event);
    }
  }

  static void mouseButtonCallback(GLFWwindow *pglfwWindow, int button, int action,
                                  int modifiers) {
    MouseEvent event;

    switch (button) {
    case GLFW_MOUSE_BUTTON_LEFT:
      event.type = (action == GLFW_PRESS) ? MouseEvent::Type::LeftBtnDown
                                          : MouseEvent::Type::LeftBtnUp;
      break;
    case GLFW_MOUSE_BUTTON_RIGHT:
      event.type = (action == GLFW_PRESS) ? MouseEvent::Type::RightBtnDown
                                          : MouseEvent::Type::RightBtnUp;
      break;
    case GLFW_MOUSE_BUTTON_MIDDLE:
      event.type = (action == GLFW_PRESS) ? MouseEvent::Type::MiddleBtnDown
                                          : MouseEvent::Type::MiddleBtnUp;
      break;
    default:
      event.type = MouseEvent::Type::Default; // this is not supported
      break;
    }

    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      event.modifiers = getInputSpecialKey(modifiers);
      double x, y;
      glfwGetCursorPos(pglfwWindow, &x, &y);
      event.pos = calcMousePos(x, y, pwindow->getMouseScale());

      pwindow->mp_callbacks->mouseEvent(event);
    }
  }

  static void mouseWheelCallback(GLFWwindow *pglfwWindow, double scrollX,
                                 double scrollY) {
    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      MouseEvent event;
      event.type = MouseEvent::Type::Wheel;
      double x, y;
      glfwGetCursorPos(pglfwWindow, &x, &y);
      event.pos = calcMousePos(x, y, pwindow->getMouseScale());
      event.wheelData = {float(scrollX), float(scrollY)};

      pwindow->mp_callbacks->mouseEvent(event);
    }
  }

  static void errorCallback(int err, const char *description) {
    std::string errMsg = std::to_string(err) + " - " + std::string(description) + '\n';
    printf(errMsg.c_str());
  }

  static void debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity,
                            GLsizei length, const GLchar *message, const void *param) {
    std::string sourceStr;
    switch (source) {
    case GL_DEBUG_SOURCE_API:
      sourceStr = "From Opengl API";
      break;
    case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
      sourceStr = "From Window system";
      break;
    case GL_DEBUG_SOURCE_THIRD_PARTY:
      sourceStr = "Form an application associated with Opengl";
      break;
    case GL_DEBUG_SOURCE_APPLICATION:
      sourceStr = "From the app itself";
      break;
    case GL_DEBUG_SOURCE_OTHER:
      sourceStr = "From other source";
      break;
    default:
      sourceStr = "Unknown";
      break;
    }

    std::string typeStr;
    switch (type) {
    case GL_DEBUG_TYPE_ERROR:
      typeStr = "An error from the OpenGL API.";
      break;
    case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
      typeStr = "Behavior that has been deprecated";
      break;
    case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
      typeStr = "Undefined behaviour GL_DEBUG_TYPE_PORTABILITIY Some functionality is "
                "not portable.";
      break;
    case GL_DEBUG_TYPE_PERFORMANCE:
      typeStr = "Possible performance issues ";
      break;
    case GL_DEBUG_TYPE_MARKER:
      typeStr = "An annotation ";
      break;
    case GL_DEBUG_TYPE_PUSH_GROUP:
      typeStr = "Messages related to debug group push.";
      break;
    case GL_DEBUG_TYPE_POP_GROUP:
      typeStr = "Messages related to debug group pop.";
      break;
    case GL_DEBUG_TYPE_OTHER:
      typeStr = "Other messages";
      break;
    default:
      typeStr = "Unknown";
      break;
    }

    std::string severityStr;
    switch (severity) {
    case GL_DEBUG_SEVERITY_HIGH:
      severityStr = "Errors or dangerous behaviour";
      break;
    case GL_DEBUG_SEVERITY_MEDIUM:
      severityStr = "Major performance warnings, other warnings or use of deprecated "
                    "functionality.";
      break;
    case GL_DEBUG_SEVERITY_LOW:
      severityStr = "Redundant state changes, unimportant undefined behaviour.";
      break;
    case GL_DEBUG_SEVERITY_NOTIFICATION:
      severityStr = "A notification, not an error or performance issue.";
      break;
    default:
      severityStr = "Unknown";
      break;
    }
  }

  static void windowFocusCallback(GLFWwindow *pglfwWindow, int focus) {
    Window *pwindow = static_cast<Window *>(glfwGetWindowUserPointer(pglfwWindow));
    if (pwindow) {
      pwindow->mp_callbacks->windowFocused();
    }
  }

private:
  static inline KeyboardEvent::Key glfwToInternalKey(int glfwKey) {
    static_assert(GLFW_KEY_ESCAPE == 256, "GLFW_KEY_ESCAPE is expected to be 256");
    if (glfwKey < GLFW_KEY_ESCAPE) {
      // Printable keys are expected to have the same value
      return (KeyboardEvent::Key)glfwKey;
    }

    switch (glfwKey) {
    case GLFW_KEY_ESCAPE:
      return KeyboardEvent::Key::Escape;
    case GLFW_KEY_ENTER:
      return KeyboardEvent::Key::Enter;
    case GLFW_KEY_TAB:
      return KeyboardEvent::Key::Tab;
    case GLFW_KEY_BACKSPACE:
      return KeyboardEvent::Key::Backspace;
    case GLFW_KEY_INSERT:
      return KeyboardEvent::Key::Insert;
    case GLFW_KEY_DELETE:
      return KeyboardEvent::Key::Del;
    case GLFW_KEY_RIGHT:
      return KeyboardEvent::Key::Right;
    case GLFW_KEY_LEFT:
      return KeyboardEvent::Key::Left;
    case GLFW_KEY_DOWN:
      return KeyboardEvent::Key::Down;
    case GLFW_KEY_UP:
      return KeyboardEvent::Key::Up;
    case GLFW_KEY_PAGE_UP:
      return KeyboardEvent::Key::PageUp;
    case GLFW_KEY_PAGE_DOWN:
      return KeyboardEvent::Key::PageDown;
    case GLFW_KEY_HOME:
      return KeyboardEvent::Key::Home;
    case GLFW_KEY_END:
      return KeyboardEvent::Key::End;
    case GLFW_KEY_CAPS_LOCK:
      return KeyboardEvent::Key::CapsLock;
    case GLFW_KEY_SCROLL_LOCK:
      return KeyboardEvent::Key::ScrollLock;
    case GLFW_KEY_NUM_LOCK:
      return KeyboardEvent::Key::NumLock;
    case GLFW_KEY_PRINT_SCREEN:
      return KeyboardEvent::Key::PrintScreen;
    case GLFW_KEY_PAUSE:
      return KeyboardEvent::Key::Pause;
    case GLFW_KEY_F1:
      return KeyboardEvent::Key::F1;
    case GLFW_KEY_F2:
      return KeyboardEvent::Key::F2;
    case GLFW_KEY_F3:
      return KeyboardEvent::Key::F3;
    case GLFW_KEY_F4:
      return KeyboardEvent::Key::F4;
    case GLFW_KEY_F5:
      return KeyboardEvent::Key::F5;
    case GLFW_KEY_F6:
      return KeyboardEvent::Key::F6;
    case GLFW_KEY_F7:
      return KeyboardEvent::Key::F7;
    case GLFW_KEY_F8:
      return KeyboardEvent::Key::F8;
    case GLFW_KEY_F9:
      return KeyboardEvent::Key::F9;
    case GLFW_KEY_F10:
      return KeyboardEvent::Key::F10;
    case GLFW_KEY_F11:
      return KeyboardEvent::Key::F11;
    case GLFW_KEY_F12:
      return KeyboardEvent::Key::F12;
    case GLFW_KEY_KP_0:
      return KeyboardEvent::Key::Keypad0;
    case GLFW_KEY_KP_1:
      return KeyboardEvent::Key::Keypad1;
    case GLFW_KEY_KP_2:
      return KeyboardEvent::Key::Keypad2;
    case GLFW_KEY_KP_3:
      return KeyboardEvent::Key::Keypad3;
    case GLFW_KEY_KP_4:
      return KeyboardEvent::Key::Keypad4;
    case GLFW_KEY_KP_5:
      return KeyboardEvent::Key::Keypad5;
    case GLFW_KEY_KP_6:
      return KeyboardEvent::Key::Keypad6;
    case GLFW_KEY_KP_7:
      return KeyboardEvent::Key::Keypad7;
    case GLFW_KEY_KP_8:
      return KeyboardEvent::Key::Keypad8;
    case GLFW_KEY_KP_9:
      return KeyboardEvent::Key::Keypad9;
    case GLFW_KEY_KP_DECIMAL:
      return KeyboardEvent::Key::KeypadDel;
    case GLFW_KEY_KP_DIVIDE:
      return KeyboardEvent::Key::KeypadDivide;
    case GLFW_KEY_KP_MULTIPLY:
      return KeyboardEvent::Key::KeypadMultiply;
    case GLFW_KEY_KP_SUBTRACT:
      return KeyboardEvent::Key::KeypadSubtract;
    case GLFW_KEY_KP_ADD:
      return KeyboardEvent::Key::KeypadAdd;
    case GLFW_KEY_KP_ENTER:
      return KeyboardEvent::Key::KeypadEnter;
    case GLFW_KEY_KP_EQUAL:
      return KeyboardEvent::Key::KeypadEqual;
    case GLFW_KEY_LEFT_SHIFT:
      return KeyboardEvent::Key::LeftShift;
    case GLFW_KEY_LEFT_CONTROL:
      return KeyboardEvent::Key::LeftControl;
    case GLFW_KEY_LEFT_ALT:
      return KeyboardEvent::Key::LeftAlt;
    case GLFW_KEY_LEFT_SUPER:
      return KeyboardEvent::Key::LeftSuper;
    case GLFW_KEY_RIGHT_SHIFT:
      return KeyboardEvent::Key::RightShift;
    case GLFW_KEY_RIGHT_CONTROL:
      return KeyboardEvent::Key::RightControl;
    case GLFW_KEY_RIGHT_ALT:
      return KeyboardEvent::Key::RightAlt;
    case GLFW_KEY_RIGHT_SUPER:
      return KeyboardEvent::Key::RightSuper;
    case GLFW_KEY_MENU:
      return KeyboardEvent::Key::Menu;
    default:
      assert(false);
      return (KeyboardEvent::Key)0;
    }
  }

  static inline InputSpecial getInputSpecialKey(int mask) {
    InputSpecial mods;
    mods.isAltDown = (mask & GLFW_MOD_ALT) != 0;
    mods.isCtrlDown = (mask & GLFW_MOD_CONTROL) != 0;
    mods.isShiftDown = (mask & GLFW_MOD_SHIFT) != 0;
    return mods;
  }

  static inline glm::vec2 calcMousePos(double xPos, double yPos,
                                       const glm::vec2 &mouseScale) {
    glm::vec2 pos = glm::vec2(float(xPos), float(yPos));
    pos *= mouseScale;
    return pos;
  }

  static inline bool prepareKeyboardEvent(int key, int action, int modifiers,
                                          KeyboardEvent &event) {
    if (action == GLFW_REPEAT || key == GLFW_KEY_UNKNOWN) {
      return false;
    }

    event.type = (action == GLFW_RELEASE) ? KeyboardEvent::Type::KeyReleased
                                          : KeyboardEvent::Type::KeyPressed;
    event.key = glfwToInternalKey(key);
    event.mode = getInputSpecialKey(modifiers);
    return true;
  }
};

Window::Window(ICallbacks *callbacks, const Des &description)
    : m_isDebug(true), mp_window(nullptr), mp_callbacks(callbacks),
      m_description(description), m_mouseScale({1.f / float(m_description.width),
                                                1.f / float(m_description.height)}) {}

Window::~Window() {
  glfwDestroyWindow(mp_window);
  glfwTerminate();
}

void Window::shutdown() { glfwSetWindowShouldClose(mp_window, 1); }

void Window::setWindowSize(uint32_t width, uint32_t height) {
  glfwSetWindowSize(mp_window, width, height);
  checkWindowSize();

  mp_callbacks->windowSizeChanged(width, height);
}

void Window::checkWindowSize() {
  int32_t actualWidth, actualHeight;
  glfwGetWindowSize(mp_window, &actualWidth, &actualHeight);

  m_description.width = uint32_t(actualWidth);
  m_description.height = uint32_t(actualHeight);
  m_mouseScale.x = 1.0f / float(m_description.width);
  m_mouseScale.y = 1.0f / float(m_description.height);
}

void Window::setWindowTitle(const std::string &title) {
  m_description.title = title;
  glfwSetWindowTitle(mp_window, m_description.title.c_str());
}

void Window::setFullScreen(bool mode) { m_description.isFullScreen = mode; }

void Window::setResizable(bool mode) {
  m_description.isResizable = mode;
  glfwSetWindowAttrib(mp_window, GLFW_RESIZABLE, int(m_description.isResizable));
}

void Window::pullEvents() { glfwPollEvents(); }

void Window::renderLoop() {
  mp_callbacks->setup();
  mp_callbacks->windowSizeChanged(m_description.width, m_description.height);

  glfwShowWindow(mp_window);
  glfwFocusWindow(mp_window);

  while (!glfwWindowShouldClose(mp_window)) {
    pullEvents();
    mp_callbacks->renderStart();
    mp_callbacks->render();
    mp_callbacks->renderEnd();
    glfwSwapBuffers(mp_window);
  }
}

std::shared_ptr<Window> Window::createWindow(ICallbacks *callbacks,
                                             const Des &description) {
  // set error call back
  glfwSetErrorCallback(ImpCallbacks::errorCallback);

  // init glfw
  if (glfwInit() == GLFW_FALSE) {
    printf("glfw init failed!");
    return nullptr;
  }

  auto pWindow = std::shared_ptr<Window>(new Window(callbacks, description));

  // Create the window
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  uint32_t w = description.width;
  uint32_t h = description.height;

  if (description.isFullScreen) {
    glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
    auto mon = glfwGetPrimaryMonitor();
    auto mod = glfwGetVideoMode(mon);
    w = mod->width;
    h = mod->height;
  }

  GLFWwindow *pGLFWWindow =
      glfwCreateWindow(w, h, description.title.c_str(), nullptr, nullptr);
  glfwMakeContextCurrent(pGLFWWindow);

  // TODO: glad setup maybe changed furture
  assert(gladLoadGL());

  if (pWindow->m_isDebug) {
    glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
    glEnable(GL_DEBUG_OUTPUT);
    glDebugMessageCallback(ImpCallbacks::debugCallback, nullptr);
  }

  if (pGLFWWindow == nullptr) {
    printf("Window creation failed!");
    return nullptr;
  }

  pWindow->mp_window = pGLFWWindow;

  pWindow->checkWindowSize();

  glfwSetWindowUserPointer(pGLFWWindow, pWindow.get());

  // Set all callbacks
  glfwSetWindowSizeCallback(pGLFWWindow, ImpCallbacks::windowSizeCallback);
  glfwSetKeyCallback(pGLFWWindow, ImpCallbacks::keyboardCallback);
  glfwSetMouseButtonCallback(pGLFWWindow, ImpCallbacks::mouseButtonCallback);
  glfwSetCursorPosCallback(pGLFWWindow, ImpCallbacks::mouseMoveCallback);
  glfwSetScrollCallback(pGLFWWindow, ImpCallbacks::mouseWheelCallback);
  glfwSetCharCallback(pGLFWWindow, ImpCallbacks::charInputCallback);
  glfwSetWindowFocusCallback(pGLFWWindow, ImpCallbacks::windowFocusCallback);

  return pWindow;
}
} // namespace Data